<?php

abstract class Jf_Tree_Container implements RecursiveIterator, Countable {

    protected static $_adapter = '';

    protected $_nodes = array();

    protected $_index = array();

    protected $_dirtyIndex = false;

    protected function _sort () {
        if ($this -> _dirtyIndex) {
            $newIndex = array();
            $index = 0;
            foreach ($this -> _nodes as $hash => $node) {
                $order = $node -> getOrder ();
                if ($order === null) {
                    $newIndex[$hash] = $index;
                    $index ++ ;
                } else {
                    $newIndex[$hash] = $order;
                }
            }
            asort ($newIndex);
            $this -> _index = $newIndex;
            $this -> _dirtyIndex = false;
        }
    }

    public function notifyOrderUpdated () {
        $this -> _dirtyIndex = true;
    }

    public function addNode ($node) {
        if ($node === $this) {
            throw new Jf_Tree_Exception ('A node cannot have itself as a parent');
        }
        $adapter = $this -> getAdapter ();
        if (empty ($adapter) ||  ! is_string ($adapter)) {
            throw new Jf_Tree_Exception ('Invalid argument: adapter');
        }
        if (is_array ($node) || $node instanceof Zend_Config) {
            $node = Jf_Tree::UrlFactory ($node, $adapter);
        } elseif ( ! $node instanceof Jf_Tree_Node) {
            throw new Jf_Tree_Exception ('Invalid argument: $node must be an instance of ' . 'Jf_Tree_Node or Zend_Config, or an array');
        }
        $hash = $node -> hashCode ();
        if (array_key_exists ($hash, $this -> _index)) {
            // node is already in container
            return $this;
        }
        // adds node to container and sets dirty flag
        $this -> _nodes[$hash] = $node;
        $this -> _index[$hash] = $node -> getOrder ();
        $this -> _dirtyIndex = true;
        // inject self as node parent
        $node -> setParent ($this);
        return $this;
    }

    public function addNodes ($nodes) {
        if ($nodes instanceof Zend_Config) {
            $nodes = $nodes -> toArray ();
        }
        if ($nodes instanceof Jf_Tree_Container) {
            $nodes = iterator_to_array ($nodes);
        }
        if ( ! is_array ($nodes)) {
            throw new Jf_Tree_Exception ('Invalid argument: $nodes must be an array, an ' . 'instance of Zend_Config or an instance of ' . 'Jf_Tree_Container');
        }
        foreach ($nodes as $node) {
            $this -> addNode ($node);
        }
        return $this;
    }

    public function setNodes (array $nodes) {
        $this -> removeNodes ();
        return $this -> addNodes ($nodes);
    }

    public function getNodes () {
        return $this -> _nodes;
    }

    public function getAdapter () {
        return self::$_adapter;
    }

    public function setAdapter ($adapter) {
        self::$_adapter = ucfirst ($adapter);
        return $this;
    }

    public function removeNode ($node) {
        if ($node instanceof Jf_Tree_Node) {
            $hash = $node -> hashCode ();
        } elseif (is_int ($node)) {
            $this -> _sort ();
            if ( ! $hash = array_search ($node, $this -> _index)) {
                return false;
            }
        } else {
            return false;
        }
        if (isset ($this -> _nodes[$hash])) {
            unset ($this -> _nodes[$hash]);
            unset ($this -> _index[$hash]);
            $this -> _dirtyIndex = true;
            return true;
        }
        return false;
    }

    public function removeNodes () {
        $this -> _nodes = array();
        $this -> _index = array();
        return $this;
    }

    public function hasNode (Jf_Tree_Node $node, $recursive = false) {
        if (array_key_exists ($node -> hashCode (), $this -> _index)) {
            return true;
        } elseif ($recursive) {
            foreach ($this -> _nodes as $childNode) {
                if ($childNode -> hasNode ($node, true)) {
                    return true;
                }
            }
        }
        return false;
    }

    public function hasNodes () {
        return count ($this -> _index) > 0;
    }

    public function findOneBy ($property, $value) {
        $iterator = new RecursiveIteratorIterator ($this, RecursiveIteratorIterator::SELF_FIRST);
        foreach ($iterator as $node) {
            if ($node -> get ($property) == $value) {
                return $node;
            }
        }
        return null;
    }

    public function findAllBy ($property, $value) {
        $found = array();
        $iterator = new RecursiveIteratorIterator ($this, RecursiveIteratorIterator::SELF_FIRST);
        foreach ($iterator as $node) {
            if ($node -> get ($property) == $value) {
                $found[] = $node;
            }
        }
        return $found;
    }

    public function findBy ($property, $value, $all = false) {
        if ($all) {
            return $this -> findAllBy ($property, $value);
        } else {
            return $this -> findOneBy ($property, $value);
        }
    }

    public function __call ($method, $arguments) {
        if (@preg_match ('/(find(?:One|All)?By)(.+)/', $method, $match)) {
            return $this -> {$match[1]} ($match[2], $arguments[0]);
        }
        throw new Jf_Tree_Exception (sprintf ('Bad method call: Unknown method %s::%s', get_class ($this), $method));
    }

    public function toArray () {
        $nodes = array();
        $this -> _dirtyIndex = true;
        $this -> _sort ();
        $indexes = array_keys ($this -> _index);
        foreach ($indexes as $hash) {
            $nodes[] = $this -> _nodes[$hash] -> toArray ();
        }
        return $nodes;
    }

    public function current () {
        $this -> _sort ();
        current ($this -> _index);
        $hash = key ($this -> _index);
        if (isset ($this -> _nodes[$hash])) {
            return $this -> _nodes[$hash];
        } else {
            throw new Jf_Tree_Exception ('Corruption detected in container; ' . 'invalid key found in internal iterator');
        }
    }

    public function key () {
        $this -> _sort ();
        return key ($this -> _index);
    }

    public function next () {
        $this -> _sort ();
        next ($this -> _index);
    }

    public function rewind () {
        $this -> _sort ();
        reset ($this -> _index);
    }

    public function valid () {
        $this -> _sort ();
        return current ($this -> _index) !== false;
    }

    public function hasChildren () {
        return $this -> hasNodes ();
    }

    public function getChildren () {
        $hash = key ($this -> _index);
        if (isset ($this -> _nodes[$hash])) {
            return $this -> _nodes[$hash];
        }
        return null;
    }

    public function count () {
        return count ($this -> _index);
    }

}
